- Published on
RAG检索增强生成(四)
- Authors

- Name
- 游戏人生
增强 RAG 能力
改写 LLM 提问
为了提高检索的质量,需要对用户的提问进行改写,让其成为一个独立的问题,包含检索的所有关键词。LLM app 遇到问题时,通常会尝试加入更多的 LLM 来解决问题。
首先定义 prompt,通过 system prompt 去给 llm 确定任务,根据聊天记录去把对话重新描述成一个独立的问题,并强调重述问题的目标:
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const rephraseChainPrompt = ChatPromptTemplate.fromMessages([
[
"system",
"给定以下对话和一个后续问题,请将后续问题重述为一个独立的问题。请注意,重述的问题应该包含足够的信息,使得没有看过对话历史的人也能理解。",
],
new MessagesPlaceholder("history"),
["human", "将以下问题重述为一个独立的问题:\n{question}"],
]);
据此构成一个 chain:
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
const rephraseChain = RunnableSequence.from([
rephraseChainPrompt,
new ChatAlibabaTongyi({
model: "qwen-turbo",
temperature: 0.2,
}),
new StringOutputParser(),
]);
测试效果:
import { HumanMessage, AIMessage } from "@langchain/core/messages";
const historyMessages = [new HumanMessage("你好,我是叮当猫"), new AIMessage("你好,叮当猫")];
const question = "你觉得我的名字怎么样?";
const standaloneQuestion = await rephraseChain.invoke({ history: historyMessages, question });
console.log(standaloneQuestion);
// 你觉得“叮当猫”这个名字如何?
可以看到,这里使用了 “我的名字” 这个代词,在 llm 的重述下,将这个替换成了 “叮当猫”。这个处理除了可以解决代词的问题,也能解决一些自然语言灵活性带来的问题,保证进行 retriver 时的问题是高质量的。
构建完整的 RAG chain
使用了 Faiss 作为本地的数据库,将 RAG 相关知识点串联起来,构建一个完整的 RAG chain,代码需要运行在 node 环境。
切割文本,并保存在本地的数据库:prepare.ts
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import "dotenv/config";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { AlibabaTongyiEmbeddings } from "@langchain/community/embeddings/alibaba_tongyi";
import path from "path";
const run = async () => {
const baseDir = __dirname;
const loader = new TextLoader(path.join(baseDir, "../../data/qiu.txt"));
const docs = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 100,
});
const splitDocs = await splitter.splitDocuments(docs);
const embeddings = new AlibabaTongyiEmbeddings();
const vectorStore = await FaissStore.fromDocuments(splitDocs, embeddings);
await vectorStore.save(path.join(baseDir, "../../db/qiu"));
};
run();
核心代码 index.ts:
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { AlibabaTongyiEmbeddings } from "@langchain/community/embeddings/alibaba_tongyi";
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import "dotenv/config";
import path from "path";
import { JSONChatHistory } from "../../JSONChatHistory/index";
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import {
RunnableSequence,
RunnablePassthrough,
RunnableWithMessageHistory,
Runnable,
} from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { Document } from "@langchain/core/documents";
/**
* 根据重写后的独立问题去读取数据库的中相关文档
*/
async function loadVectorStore() {
const directory = path.join(__dirname, "../../db/qiu");
const embeddings = new AlibabaTongyiEmbeddings();
const vectorStore = await FaissStore.load(directory, embeddings);
return vectorStore;
}
async function getRephraseChain() {
const rephraseChainPrompt = ChatPromptTemplate.fromMessages([
[
"system",
"给定以下对话和一个后续问题,请将后续问题重述为一个独立的问题。请注意,重述的问题应该包含足够的信息,使得没有看过对话历史的人也能理解。",
],
new MessagesPlaceholder("history"),
["human", "将以下问题重述为一个独立的问题:\n{question}"],
]);
const rephraseChain = RunnableSequence.from([
rephraseChainPrompt,
new ChatAlibabaTongyi({
model: "qwen-turbo",
temperature: 0.4,
}),
new StringOutputParser(),
]);
return rephraseChain;
}
async function testRephraseChain() {
const historyMessages = [
new HumanMessage("你好,我是叮当猫"),
new AIMessage("你好叮当猫"),
];
const rephraseChain = await getRephraseChain();
const question = "你觉得我的名字怎么样?";
const standaloneQuestion = await rephraseChain.invoke({
history: historyMessages,
question,
});
console.log(standaloneQuestion);
}
export async function getRagChain(): Promise<Runnable> {
const vectorStore = await loadVectorStore();
const retriever = vectorStore.asRetriever(2);
/**
* 使用 retriever 获取相关文档,然后转换成纯字符串。
* @param documents
* @returns
*/
const convertDocsToString = (documents: Document[]): string => {
return documents.map((document) => document.pageContent).join("\n");
};
const contextRetrieverChain = RunnableSequence.from([
(input) => input.standalone_question,
retriever,
convertDocsToString,
]);
const SYSTEM_TEMPLATE = `
你是一个熟读刘慈欣的《球状闪电》的终极原着党,精通根据作品原文详细解释和回答问题,你在回答时会引用作品原文。
并且回答时仅根据原文,尽可能回答用户问题,如果原文中没有相关内容,你可以回答“原文中没有相关内容”,
以下是原文中跟用户回答相关的内容:
{context}
`;
/**
* 包含历史记录信息的 prompt
*/
const prompt = ChatPromptTemplate.fromMessages([
["system", SYSTEM_TEMPLATE],
new MessagesPlaceholder("history"),
["human", "现在,你需要基于原文,回答以下问题:\n{standalone_question}`"],
]);
const model = new ChatAlibabaTongyi({
model: "qwen-turbo",
});
const rephraseChain = await getRephraseChain();
/**
* 改写提问 => 根据改写后的提问获取文档 => 生成回复 的 rag chain
*/
const ragChain = RunnableSequence.from([
RunnablePassthrough.assign({
standalone_question: rephraseChain,
}),
RunnablePassthrough.assign({
context: contextRetrieverChain,
}),
prompt,
model,
new StringOutputParser(),
]);
const chatHistoryDir = path.join(__dirname, "../../chat_data");
/**
* 使用 RunnableWithMessageHistory 去管理 history,给 chain 增加聊天记录的功能
* 传给 getMessageHistory 的函数,需要根据用户传入的 sessionId 去获取初始的 chat history
*/
const ragChainWithHistory = new RunnableWithMessageHistory({
runnable: ragChain,
getMessageHistory: (sessionId) =>
new JSONChatHistory({ sessionId, dir: chatHistoryDir }),
historyMessagesKey: "history",
inputMessagesKey: "question",
});
return ragChainWithHistory;
}
async function run() {
const ragChain = await getRagChain();
const res = await ragChain.invoke(
{
question: "什么是球状闪电?",
},
{
configurable: { sessionId: "test-history" },
}
);
console.log(res);
}
测试代码,首先执行 prepare.ts 文件,切割文本,保存到本地的 docstore.json 中:
ts-node ./node/rag/prepare.ts
执行 index.ts 文件,在 index.ts 文件中,先增加函数执行 run(),在执行文件:
ts-node ./node/rag/index.ts
输出内容如下:
球状闪电是刘慈欣作品《球状闪电》中的关键概念,它是一种自然现象,表现为一种明亮的、球形或椭圆形的发光体,通常出现在雷暴天气中。书中提到球状闪电的行为和特性是基于真实历史记录的描述,它们具有量子性质,在失去观察者后会消失并重新生成,显示出超乎寻常的物理特性。在小说中,球状闪电的研究被用于开发一种高度先进的武器系统,但其基本原理和技术细节并未完全揭示给读者。
至此,就完成了一个非常完整的 rag chain,有自动的提问改写、数据检索、聊天记录等基本的功能。
部署成 API
将开发完成的 chain 部署成 API,方便用户调用。新建 server.ts 文件,代码如下:
import express from "express";
import { getRagChain } from ".";
const app = express();
const port = 3001;
app.use(express.json());
app.post("/", async (req, res) => {
const ragChain = await getRagChain();
const body = req.body;
const result = await ragChain.stream(
{
question: body.question,
},
{ configurable: { sessionId: body.session_id } }
);
res.set("Content-Type", "text/plain");
for await (const chunk of result) {
res.write(chunk);
}
res.end();
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
启动服务器:
ts-node ./node/rag/server.ts
新增 client.ts 文件,调用 API:
const port = 3001;
async function fetchStream() {
const response = await fetch(`http://localhost:${port}`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
question: "什么是球状闪电",
session_id: "test-server",
}),
});
const reader = response?.body?.getReader();
const decoder = new TextDecoder();
while (true && reader) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}
console.log("Stream has ended");
}
fetchStream();
执行 client.ts,输出内容如下:
球
状
闪电
是刘慈欣作品
《球状闪电》中的关键概念
,它是一种自然现象,表现为一种
明亮的、球形或椭圆形
的发光体,通常出现在雷暴
天气中。书中提到球状闪电
的行为和特性是基于真实历史记录
的描述,它们具有量子性质,在
失去观察者后会消失并重新
生成,显示出超乎寻常的物理
特性。在小说中,球状
闪电的研究被用于揭示微观世界的秘密
,尽管其军事应用相对较小。
Stream has ended
到此就完成了一个简单的 rag chain,通过调用 API,实现了问答系统的功能。